#include "bzbfile/file.h"
#include "bzbstring/stringutils.h"
#include "bzbutil/platform.h"

#if defined(WINDOWS)

#undef _WIN32_WINNT
#define _WIN32_WINNT 0x0601
#define WINDOWS_LEAN_AND_MEAN 1
#include <windows.h>
#include <wincrypt.h>
#include <tlhelp32.h>

#elif defined(LINUX)

#include <cstring>
#include <unistd.h>
#include <sys/uio.h>
#include <linux/random.h>
#include <linux/limits.h>
#include <signal.h>
#include <dirent.h>
#include <syscall.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <ftw.h>

#endif


#include <fstream>

using namespace bzbfile;
using namespace bzbstring;


std::string bzbfile::path(const std::string &path) {

	if(!path.size()) {
		return "";
	}

	size_t index = 0;
	std::string fpath = to_unix_path(path);
	const size_t size = fpath.size();
	bool flag = false;

	for (size_t i = size - 1; i >= 0; --i) {
		if(fpath[i] == '/') {
			index = i + 1;
			flag = true;
			break;
		}
	}

	if(flag) {
		return fpath.substr(0, index ? index : 1);
	}

	return "";
}


std::string bzbfile::extension(const std::string &filename) {

	if(!filename.size()) {
		return "";
	}

	size_t index = 0;
	const size_t size = filename.size();
	bool flag = false;

	for (size_t i = size - 1; i >= 0; --i) {
		if(filename[i] == '.') {
			index = i;
			flag = true;
			break;
		} else if(filename[i] == '/' || filename[i] == '\\') {
			break;
		}
	}

	if(flag) {
		return filename.substr(index + 1, size - index - 1);
	}

	return "";
}


std::string bzbfile::name(const std::string &path) {

	if(!path.size()) {
		return "";
	}

	size_t index = 0;
    std::string fpath = to_unix_path(path);
	const size_t size = fpath.size();
	bool flag = false;

	for (size_t i = 0; i < size; ++i) {
		if(fpath[i] == '/') {
			index = i;
			flag = true;
		}
	}

	if(flag) {
		return fpath.substr(index + 1, size - index - 1);
	}

	return fpath;
}


std::string bzbfile::basename(const std::string &filename) {

	std::string ffilename = bzbfile::name(filename);
	const size_t size = ffilename.size();
	size_t index = 0;

	if(!size) {
		return "";
	}

	for (size_t i = 0; i < size; ++i) {
		if(ffilename[i] == '.') {
			index = i;
		}
	}

	if(index) {
		return ffilename.substr(0, index);
	}

	return ffilename;
}


bool bzbfile::exists(const std::string &filename) {

	FILE *file = fopen(filename.c_str(), "r");

	if(file) {
		fclose(file);
		return true;
	} else {
		return false;
	}
}


bool bzbfile::exists_dir(const std::string &dir) {

#if defined(WINDOWS)

	DWORD dwAttributes = GetFileAttributes(dir.c_str());

	return (dwAttributes != INVALID_FILE_ATTRIBUTES) &&
			(dwAttributes &  FILE_ATTRIBUTE_DIRECTORY);

#elif defined(LINUX)

	return !access(dir.c_str(), F_OK);

#endif

}


int64_t bzbfile::size(const std::string &filename) {

#if defined(WINDOWS)

	HANDLE fd = CreateFile(filename.c_str(), GENERIC_READ, 0, nullptr,
							OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
	if(!fd) {
		bzbutil::throw_sys_error();
	}

	LARGE_INTEGER size_lg;
	int err = GetFileSizeEx(fd, &size_lg);

	if(!err) {
		bzbutil::throw_sys_error();
	}

	CloseHandle(fd);

	return size_lg.QuadPart;

#elif defined(LINUX)

	struct stat st;
	if(stat(filename.c_str(), &st) == -1) {
		bzbutil::throw_sys_error();
	}

	return static_cast<int64_t>(st.st_size);

#endif

}


std::string bzbfile::readlink(const std::string &symlink) {

#ifdef WINDOWS

	HANDLE handle = CreateFile(symlink.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ,
								0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS |
								FILE_ATTRIBUTE_REPARSE_POINT, 0);

	// TO-DO Check with file attributes if valid symlink (FILE_ATTRIBUTE_REPARSE_POINT)

	DWORD dwAttributes = GetFileAttributes(symlink.c_str());

	if(!dwAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
		throw -1;
	}

	char buffer[260];

	GetFinalPathNameByHandle(handle, buffer, 260, FILE_NAME_NORMALIZED);

	return buffer;

#elif defined(LINUX)

	struct stat link_stat;

	ssize_t res = lstat(symlink.c_str(), &link_stat);
	if(res == -1) {
		bzbutil::throw_sys_error();
	}

	char buffer[PATH_MAX];
	res = ::readlink(symlink.c_str(), buffer, PATH_MAX);
	if(res == -1) {
		bzbutil::throw_sys_error();
	}

	buffer[res] = '\0';
	return std::string(buffer);

#endif

}


void bzbfile::remove(const std::string &filename) {

#if defined(WINDOWS)

	int res = DeleteFile(filename.c_str());
	if(!res) {
		bzbutil::throw_sys_error();
	}

#elif defined(LINUX)

	int res = unlink(filename.c_str());
	if(res) {
		bzbutil::throw_sys_error();
	}

#endif

}


#ifdef LINUX
int remove_callback(const char *filepath,
					const struct stat *st,
					int flag,
					struct FTW *buffer) {
	
	return ::remove(filepath);
}
#endif

void bzbfile::remove_dir(const std::string &dir) {

#if defined(WINDOWS)

	char buffer[260];
	snprintf(buffer, 260, "%s", dir.c_str());

	size_t index = dir.size() >= 258 ? 258 : dir.size();
	buffer[index] = '\0';
	buffer[index + 1] = '\0';

	SHFILEOPSTRUCT opstruct = {
		nullptr,
		FO_DELETE,
		buffer,
		"\0\0", // Doubly null terminated
		FOF_NOCONFIRMATION |
		FOF_NOERRORUI |
		FOF_SILENT,
		false,
		0,
		nullptr
	};

	int res = SHFileOperation(&opstruct);

	if(res || opstruct.fAnyOperationsAborted) {
		bzbutil::throw_sys_error(res);
	}

	// TO-DO This code doesn't work (ERROR_INVALID_PARAMETER)

#elif defined(LINUX)

	int res = nftw(dir.c_str(), remove_callback, 64, FTW_DEPTH | FTW_PHYS);
	if(res) {
		bzbutil::throw_sys_error();
	}

#endif

}


std::string bzbfile::read(const std::string &filename) {

	std::ifstream stream(filename);

	if(!stream.is_open()) {
		bzbutil::throw_sys_error();
	}

	std::stringstream ss;
	ss << stream.rdbuf();
	return ss.str();
}


std::vector<char> bzbfile::read_bin(const std::string &filename,
									uint64_t size,
									uint64_t offset) {

	std::ifstream stream(filename, std::ios::binary);

	std::vector<char> data;

	if(stream) {

		if(!size) {
			size = bzbfile::size(filename) - offset;
		}

		data.resize(size);
		if(data.size() != size) {
			bzbutil::throw_sys_error();
		}

		if(offset) {
			stream.seekg(offset);
		}

		stream.read(&data[0], size);
		stream.close();
		return data;
	} else {
		bzbutil::throw_sys_error(errno);
	}

	return std::vector<char>();
}


void bzbfile::write(const std::string &filename,
					const std::string &data,
					bool truncate) {

	std::ofstream stream(filename,
						(truncate ? std::ios::out |
						std::ios::trunc :
						std::ios::out));

	if(!stream.is_open()) {
		bzbutil::throw_sys_error(errno);
	}

	stream << data;
}


std::size_t bzbfile::write_bin(	const std::string &filename,
							const std::vector<char> &data,
							bool truncate) {

	std::ofstream stream(filename,
						(truncate ? std::ios::binary |
						std::ios::trunc :
						std::ios::binary));

	if(stream) {

		stream.write(&data[0], data.size());
		stream.close();
		// TO-DO Checking byte count
		return data.size();
	} else {
		bzbutil::throw_sys_error(errno);
	}

	return 0;
}


size_t bzbfile::write_bin(const std::string &filename,
						uint64_t offset,
						const std::vector<char> &data) {

	std::ofstream stream(filename, std::ios::binary | std::ios::in | std::ios::ate);

	if(stream) {

		if(offset) {
			stream.seekp(offset);
		} else {
			stream.seekp(offset);
		}

		stream.write(&data[0], data.size());
		stream.close();
		// TO-DO Checking byte count
		return data.size();
	} else {
		bzbutil::throw_sys_error(errno);
	}

	return 0;
}


std::vector<std::string> bzbfile::get_dir_file_list(const std::string &dir) {

	std::vector<std::string> output;

#if defined(WINDOWS)

	HANDLE hFile;
	WIN32_FIND_DATA wfd;
	TCHAR path_ch[_MAX_PATH];

	// Compatible with Unix and Windows string format
	std::string path = dir;
	std::replace(path.begin(), path.end(), '/', '\\');
	path.append("\\*");

	path_ch[path.size()] = '\0';
	std::copy(path.begin(), path.end(), path_ch);

	hFile = FindFirstFile(path_ch, &wfd);
	if(hFile == INVALID_HANDLE_VALUE) {
		bzbutil::throw_sys_error();
	}

	do {
		if(!(wfd.dwFileAttributes & FILE_ATTRIBUTE_NORMAL)) {

			output.emplace_back(wfd.cFileName);
		}
	} while(FindNextFile(hFile, &wfd) != 0);

	CloseHandle(hFile);

	return output;

#elif defined(LINUX)

	DIR* handle = opendir(dir.c_str());
	if(!handle) {
		bzbutil::throw_sys_error();
	}

	dirent *dir_entry;
	while((dir_entry = readdir(handle))) {
		if(dir_entry->d_type == DT_REG) {
			output.emplace_back(dir_entry->d_name);
		}
	}

	int error = closedir(handle);

	if (error) {
		bzbutil::throw_sys_error();
	}

	return output;

#endif

}


std::vector<std::string> bzbfile::get_dir_folder_list(const std::string &dir) {

	std::vector<std::string> output;

#if defined(WINDOWS)

	HANDLE hFile;
	WIN32_FIND_DATA wfd;
	TCHAR path_ch[_MAX_PATH];

	// Compatible with Unix and Windows string format
	std::string path = dir;
	std::replace(path.begin(), path.end(), '/', '\\');
	path.append("\\*");

	path_ch[path.size()] = '\0';
	std::copy(path.begin(), path.end(), static_cast<char*>(path_ch));

	hFile = FindFirstFile(path_ch, &wfd);
	if(hFile == INVALID_HANDLE_VALUE) {
		bzbutil::throw_sys_error();
	}

	do {
		if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
			 std::string(wfd.cFileName) != "." &&
			 std::string(wfd.cFileName) != "..") {

			output.emplace_back(wfd.cFileName);
		}
	} while(FindNextFile(hFile, &wfd) != 0);

	CloseHandle(hFile);

	return output;

#elif defined(LINUX)

	DIR *handle = opendir(dir.c_str());
	if(!handle) {
		bzbutil::throw_sys_error();
	}

	dirent *dir_entry;
	while((dir_entry = readdir(handle))) {
		if(dir_entry->d_type == DT_DIR &&
			strcmp(dir_entry->d_name, ".") != 0 &&
			strcmp(dir_entry->d_name, "..") != 0) {
			output.emplace_back(dir_entry->d_name);
		}
	}

	closedir(handle);

	return output;

#endif

}

std::string bzbfile::get_exe_path() {

#if defined(WINDOWS)

	char buffer[256];
	int res = GetModuleFileName(nullptr, buffer, 256);

	if(!res) {
		bzbutil::throw_sys_error();
	}

	return buffer;

#elif defined(LINUX)

	return bzbfile::readlink(
				bzbstring::concat("/proc/", getpid(), "/exe"));

#endif

}
